iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0
Software Development

Android 鐵人賽: 天命最高 - 陪伴大家一步步打造屬於自己的app系列 第 7

Kotlin 抽象類別、介面與 Object:從規格設計到單例模式

  • 分享至 

  • xImage
  •  

Kotlin 抽象類別、介面與 Object:從規格設計到單例模式

前一天介紹了 Kotlin 的繼承、覆寫與多型。今天要繼續學習物件導向的重要工具:抽象類別、介面、巢狀類別、內部類別、單例物件、伴生物件、匿名物件與函式式介面。這些語法在 Android 的畫面事件、資料管理、Fragment 建立與架構設計中都很常見。
參考資料:Kotlin 類別


一、為什麼需要抽象類別?

假設我們要建立一套繪圖系統,所有圖形都必須計算面積,但不同圖形的計算公式不同:

  • 圓形:半徑 × 半徑 × π
  • 矩形:寬 × 高
  • 三角形:底 × 高 ÷ 2

我們可以先建立一個共同的 Shape 父類別,但父類別本身並不知道面積該如何計算,因此可以將它宣告為抽象類別。
參考資料:Kotlin 抽象類別

abstract class Shape {
    abstract fun area(): Double
}

abstract 表示尚未完成、必須由子類別實作。抽象類別不能直接建立物件,因此下面的程式會發生錯誤。
參考資料:Kotlin 抽象類別

// 錯誤:抽象類別不能直接建立物件
// val shape = Shape()

二、抽象屬性與抽象方法

抽象父類別,裡面同時包含抽象屬性、一般屬性、抽象方法與一般方法。
參考資料:Kotlin 抽象類別

abstract class Base {

    // 抽象屬性沒有初始值,必須由子類別實作
    abstract var a: Int

    // 一般屬性可以直接提供初始值
    var b: Int = 0

    // 抽象方法沒有方法內容
    abstract fun fun1()

    // 一般方法可以直接提供方法內容
    fun fun2() {
        println("From Base class")
    }
}

抽象類別不代表所有成員都必須是抽象的。它可以同時具有完整實作與尚未實作的成員。
參考資料:Kotlin 抽象類別


三、繼承抽象類別

普通類別繼承抽象類別後,必須使用 override 完成所有抽象成員。
參考資料:Kotlin 覆寫成員

class Derived : Base() {

    // 完成父類別要求的抽象屬性
    override var a: Int = 0

    // 完成父類別要求的抽象方法
    override fun fun1() {
        println("From Derived class")
    }
}

完整測試程式如下:
參考資料:Kotlin 繼承

fun main() {
    val myClass = Derived()

    println("a = ${myClass.a}")
    println("b = ${myClass.b}")

    myClass.fun1()
    myClass.fun2()
}

執行結果:

a = 0
b = 0
From Derived class
From Base class

原始範例將函式放進 println()

println(myClass.fun1())

因為 fun1() 沒有回傳值,其回傳型別是 Unit,因此會額外印出 kotlin.Unit。若只想執行方法,直接呼叫 myClass.fun1() 即可。
參考資料:Kotlin Unit


四、抽象類別實例:計算圖形面積

圖形範例,我們可以將原本的普通父類別改為抽象類別,使設計更加明確。
參考資料:Kotlin 抽象類別

import kotlin.math.PI

// 抽象父類別:規定所有圖形都必須提供 area()
abstract class Shape {

    // 父類別不知道公式,因此交給子類別完成
    abstract fun area(): Double
}

// 圓形繼承 Shape
class Circle(
    val radius: Double
) : Shape() {

    override fun area(): Double {
        return PI * radius * radius
    }
}

// 矩形繼承 Shape
class Rectangle(
    val width: Double,
    val height: Double
) : Shape() {

    override fun area(): Double {
        return width * height
    }
}

fun main() {
    // 使用父類別型別接收不同子類別物件
    val circle: Shape = Circle(radius = 5.0)

    val rectangle: Shape = Rectangle(
        width = 10.0,
        height = 4.0
    )

    println(
        "圓形面積:${String.format("%.2f", circle.area())}"
    )

    println(
        "矩形面積:${String.format("%.2f", rectangle.area())}"
    )
}

執行結果:

圓形面積:78.54
矩形面積:40.00

雖然 circlerectangle 的變數型別都是 Shape,實際執行 area() 時,仍會依照物件的真實類型呼叫對應版本,這就是多型。
參考資料:Kotlin 覆寫方法


五、什麼是介面 Interface?

介面可以看成一份「類別必須遵守的規格」。介面可以宣告抽象方法、抽象屬性,也可以提供具有方法內容的預設實作。
參考資料:Kotlin 介面

例如,成績單必須具有數學成績、英文成績與顯示成績的方法:
參考資料:Kotlin 介面屬性

interface IntB {

    // 介面中的抽象屬性
    var math: Int
    var eng: Int

    // 介面可以提供具有內容的方法
    fun show() {
        println("math = $math, eng = $eng")
    }
}

由於 matheng 沒有初始值,也沒有提供 get() 存取器,因此實作此介面的類別必須完成這兩個屬性。
參考資料:Kotlin 介面屬性

class Report(
    override var math: Int = 0,
    override var eng: Int = 0
) : IntB

Report 沒有重新實作 show(),所以會直接使用介面提供的預設版本。
參考資料:Kotlin 介面

fun main() {
    val report = Report(
        math = 95,
        eng = 88
    )

    report.show()
}

執行結果:

math = 95, eng = 88

六、實作介面的方法

如果介面中的方法沒有方法內容,實作類別就必須使用 override 完成它。
參考資料:Kotlin 實作介面

interface Drawable {
    fun draw()
}

建立 SimpleCircle 實作 Drawable
參考資料:Kotlin 實作介面

class SimpleCircle : Drawable {

    override fun draw() {
        println("Drawing a circle")
    }
}

測試程式:

fun main() {
    val circle = SimpleCircle()
    circle.draw()
}

執行結果:

Drawing a circle

七、一個類別可以實作多個介面

Kotlin 的類別只能直接繼承一個父類別,但可以實作多個介面。多個介面之間使用逗號分隔。
參考資料:Kotlin 實作多個介面

interface Drawable {
    fun draw()
}

interface Clickable {
    fun click()
}

open class Button : Drawable, Clickable {

    override fun draw() {
        println("Drawing a button")
    }

    override fun click() {
        println("Button clicked")
    }
}

測試程式如下:

fun main() {
    val button = Button()

    button.draw()
    button.click()
}

執行結果:

Drawing a button
Button clicked

這種設計可以把不同的能力拆開。例如一個按鈕可以同時具有「繪製」與「點擊」能力,不需要把所有功能都塞進同一個父類別。
參考資料:Kotlin 介面


八、介面與多型:付款系統

我們 建立 PaymentMethod 付款方式介面。所有付款方式都必須實作 pay()
參考資料:Kotlin 介面

interface PaymentMethod {

    // amount 是付款金額
    // 回傳 Boolean 表示付款是否成功
    fun pay(amount: Int): Boolean
}

信用卡與現金雖然付款流程不同,但都可以遵守相同的 PaymentMethod 規格。
參考資料:Kotlin 實作介面

class CreditCard : PaymentMethod {

    init {
        println("建立 CreditCard 物件")
    }

    override fun pay(amount: Int): Boolean {
        if (amount <= 0) {
            println("信用卡付款失敗:金額必須大於 0")
            return false
        }

        println("使用信用卡付款:$amount 元")
        return true
    }
}

class Cash : PaymentMethod {

    init {
        println("建立 Cash 物件")
    }

    override fun pay(amount: Int): Boolean {
        if (amount <= 0) {
            println("現金付款失敗:金額必須大於 0")
            return false
        }

        println("使用現金付款:$amount 元")
        return true
    }
}

接著建立 Checkout。它不必知道傳進來的是現金還是信用卡,只需要知道對方遵守 PaymentMethod 介面。
參考資料:Kotlin 介面與多型

class Checkout {

    init {
        println("建立 Checkout 物件")
    }

    fun processPayment(
        amount: Int,
        method: PaymentMethod
    ) {
        val result = method.pay(amount)

        if (result) {
            println("結帳系統:付款完成")
        } else {
            println("結帳系統:付款失敗")
        }
    }
}

完整主程式:

fun main() {
    print("請輸入信用卡付款金額:")
    val creditAmount = readln().toIntOrNull() ?: 0

    print("請輸入現金付款金額:")
    val cashAmount = readln().toIntOrNull() ?: 0

    println()
    println("執行付款流程")

    val checkout = Checkout()
    val creditCard = CreditCard()
    val cash = Cash()

    println()

    checkout.processPayment(
        amount = creditAmount,
        method = creditCard
    )

    println()

    checkout.processPayment(
        amount = cashAmount,
        method = cash
    )
}

未來如果加入 LINE Pay、Apple Pay 或悠遊卡,只要建立新的類別實作 PaymentMethod,就不需要修改 Checkout。這正是介面帶來的擴充性。
參考資料:Kotlin 介面


九、抽象類別與介面的差別

抽象類別與介面都可以制定子類別應遵守的規則,但用途並不完全相同。
參考資料:Kotlin 抽象類別Kotlin 介面

比較項目 抽象類別 介面
宣告方式 abstract class interface
是否能建立物件 不可以 不可以
是否能有建構子 可以 不可以
是否能保存物件狀態 可以 介面本身不能保存狀態
是否能有一般方法 可以 可以
是否能有抽象方法 可以 可以
類別可使用數量 只能繼承一個父類別 可以實作多個介面
適合用途 表達「是什麼」 表達「能做什麼」

例如:

  • Circle 是一種 Shape,適合使用抽象類別。
  • Button 能夠被點擊、能夠被繪製,適合使用介面。
  • CreditCardCash 都能付款,因此共同實作 PaymentMethod

參考資料:Kotlin 類別與介面


十、巢狀類別 Nested Class

Kotlin 可以在類別裡面再宣告類別,這種設計稱為巢狀類別。
參考資料:Kotlin 巢狀與內部類別

class Outer {

    val message = "Hello from Outer"

    class Nested {

        fun greet(): String {
            return "Hello from Nested"
        }
    }
}

建立巢狀類別時,不必先建立外部類別物件,可以直接使用 Outer.Nested()
參考資料:Kotlin 巢狀類別

fun main() {
    val nested = Outer.Nested()

    println(nested.greet())
}

巢狀類別不會持有外部類別物件的參考,因此不能直接存取外部類別的 message
參考資料:Kotlin 巢狀類別

class Outer {

    val message = "Hello from Outer"

    class Nested {

        fun greet(): String {
            // 錯誤:Nested 無法直接存取 message
            // return message

            return "Hello from Nested"
        }
    }
}

十一、內部類別 Inner Class

如果內層類別需要存取外部類別的屬性與方法,必須在類別前面加上 inner
參考資料:Kotlin 內部類別

class OuterIn {

    val message = "Hello from OuterIn"

    fun outerFun() {
        println("This is an OuterIn function")
    }

    inner class Inner {

        // 可以直接存取外部類別的 message
        fun greet(): String {
            return "Message is $message"
        }

        // 可以直接呼叫外部類別的方法
        fun innerFun() {
            outerFun()
        }
    }
}

建立 Inner 前必須先建立 OuterIn 物件,因為每個 Inner 都會與某個外部物件產生關聯。
參考資料:Kotlin 內部類別

fun main() {
    val outerIn = OuterIn()

    val inner = outerIn.Inner()

    println(inner.greet())
    inner.innerFun()
}

執行結果:

Message is Hello from OuterIn
This is an OuterIn function

十二、object 單例物件

一般類別可以重複建立多個物件;object 宣告則會建立單一實例,整個程式共用同一個物件,因此常被稱為單例模式。
參考資料:Kotlin Object 宣告

object Logger {

    private var count: Int = 0

    init {
        println("Welcome to Logger")
    }

    fun log(message: String) {
        println("Log:$message")
        count++
    }

    fun logCount(): Int {
        return count
    }
}

使用 object 時不需要加上小括號建立物件,直接使用物件名稱即可。
參考資料:Kotlin Object 宣告

fun main() {
    Logger.log("App 啟動")
    Logger.log("使用者完成登入")

    println("目前紀錄數量:${Logger.logCount()}")
}

執行結果:

Welcome to Logger
Log:App 啟動
Log:使用者完成登入
目前紀錄數量:2

無論呼叫多少次 Logger,使用的都是同一個物件,所以 count 會持續累加。Kotlin 的具名 object 會在第一次存取時初始化,而且初始化具備執行緒安全性。
參考資料:Kotlin Object 初始化


十三、使用 object 製作亂數產生器

我們可以建立一個共用的亂數產生器。類別名稱依 Kotlin 慣例調整為大寫開頭的 RandomNumberGenerator
參考資料:Kotlin 編碼慣例

object RandomNumberGenerator {

    var amount: Int = 1
        set(value) {
            // 數量小於 1 時,自動修正成 1
            field = if (value < 1) 1 else value
        }

    val id: Int = 100

    fun generateNumbers(): IntArray {
        return IntArray(amount) {
            (1..100).random()
        }
    }
}

完整測試程式:

fun main() {
    RandomNumberGenerator.amount = 5

    val numbers =
        RandomNumberGenerator.generateNumbers()

    numbers.forEachIndexed { index, number ->
        println("numbers[$index] = $number")
    }

    println(
        "產生器 ID:${RandomNumberGenerator.id}"
    )
}

IntArray(amount) { ... } 的 Lambda 會依序建立每一個元素,因此不需要先建立陣列再使用 for 迴圈賦值。
參考資料:Kotlin IntArray


十四、伴生物件 Companion Object

companion object 是宣告在類別內的物件,可以保存常數、共用函式或工廠方法。呼叫時可直接透過類別名稱使用。
參考資料:Kotlin 伴生物件

class RandomNumber {

    companion object {

        fun getRandomNumber(
            amount: Int = 1
        ): IntArray {
            // 【修正】避免傳入 0 或負數
            val safeAmount =
                if (amount < 1) 1 else amount

            return IntArray(safeAmount) {
                (1..5).random()
            }
        }
    }
}

呼叫時不需要建立 RandomNumber()
參考資料:Kotlin 伴生物件

fun main() {
    val numbers =
        RandomNumber.getRandomNumber(5)

    numbers.forEachIndexed { index, value ->
        println("numbers[$index] = $value")
    }
}

雖然伴生物件的呼叫方式看起來像 Java 的 static,但它實際上仍是伴生物件的成員。
參考資料:Kotlin 伴生物件


十五、伴生物件保存常數

Android 開發中經常使用伴生物件保存 TAG、請求代碼或固定參數名稱。
參考資料:Kotlin 伴生物件

class User(
    val name: String
) {
    companion object {

        // 編譯階段常數
        const val TAG = "User"

        // 工廠方法
        fun create(name: String): User {
            return User(name)
        }
    }

    fun userInfo() {
        println("TAG = $TAG, name = $name")
    }
}

測試程式:

fun main() {
    val user = User.create("Alice")

    user.userInfo()

    println("類別標籤:${User.TAG}")
}

執行結果:

TAG = User, name = Alice
類別標籤:User

十六、Android Fragment 常見的 newInstance()

Android 的 Fragment 經常透過伴生物件提供 newInstance() 工廠方法。
參考資料:Android Fragment

下面先使用一般 Kotlin 類別模擬 Fragment:
參考資料:Kotlin 伴生物件

open class Fragment

class DetailFragment : Fragment() {

    var param1: String? = null
    var param2: String? = null

    companion object {

        fun newInstance(
            param1: String,
            param2: String
        ): DetailFragment {
            return DetailFragment().apply {
                this.param1 = param1
                this.param2 = param2
            }
        }
    }
}

測試程式:

fun main() {
    val fragment =
        DetailFragment.newInstance(
            param1 = "Value 1",
            param2 = "Value 2"
        )

    println("param1 = ${fragment.param1}")
    println("param2 = ${fragment.param2}")
}

真正的 Android Fragment 通常應透過 argumentsBundle 傳遞資料,避免 Fragment 被系統重新建立後資料消失。
參考資料:Android Fragment 傳遞參數

class DetailFragment : Fragment() {

    companion object {
        private const val ARG_PARAM_1 = "param1"
        private const val ARG_PARAM_2 = "param2"

        fun newInstance(
            param1: String,
            param2: String
        ): DetailFragment {
            return DetailFragment().apply {
                arguments = Bundle().apply {
                    putString(ARG_PARAM_1, param1)
                    putString(ARG_PARAM_2, param2)
                }
            }
        }
    }
}

十七、匿名物件 Object Expression

如果某個類別或介面的實作只會使用一次,就不一定要另外建立具名類別,可以使用匿名物件。
參考資料:Kotlin 匿名物件

先建立可以被繼承的 Car
參考資料:Kotlin 繼承

open class Car(
    val brand: String
) {
    open fun basicInfo(
        salesperson: String
    ): String {
        println(
            "Car brand = $brand, " +
                "salesperson = $salesperson"
        )

        return "Salesperson is $salesperson"
    }
}

使用匿名物件建立一次性的 Car 子類別:
參考資料:Kotlin 匿名物件繼承

fun main() {
    val car = object : Car("Toyota") {

        override fun basicInfo(
            salesperson: String
        ): String {
            println("This car brand is $brand")

            return "Salesperson is $salesperson"
        }
    }

    println(
        car.basicInfo("Mary")
    )
}

object : Car("Toyota") 代表建立一個沒有名稱的子類別,並立即建立它的物件。
參考資料:Kotlin Object Expression


十八、匿名物件實作介面

匿名物件也能直接實作介面,這種寫法常用在按鈕監聽器或回呼函式。
參考資料:Kotlin 匿名物件

interface Click {
    fun click()
}

使用匿名物件完成 Click
參考資料:Kotlin 匿名物件

fun main() {
    val button = object : Click {

        override fun click() {
            println("Button clicked")
        }
    }

    button.click()
}

也可以直接把匿名物件當成函式參數:
參考資料:Kotlin Object Expression

fun executeClick(click: Click) {
    click.click()
}

fun main() {
    executeClick(
        object : Click {
            override fun click() {
                println("匿名物件被點擊")
            }
        }
    )
}

十九、函式式介面 fun interface

如果介面只有一個抽象方法,可以在 interface 前面加上 fun,成為函式式介面,也稱為 SAM 介面。SAM 是 Single Abstract Method 的縮寫。
參考資料:Kotlin 函式式介面

fun interface Action {
    fun execute()
}

建立接收 Action 的函式:
參考資料:Kotlin SAM 轉換

fun runAction(action: Action) {
    println("開始執行 runAction")

    action.execute()

    println("結束執行 runAction")
}

傳統做法是建立匿名物件:
參考資料:Kotlin 匿名物件

fun main() {
    runAction(
        object : Action {
            override fun execute() {
                println("使用匿名物件執行")
            }
        }
    )
}

使用函式式介面後,可以透過 Lambda 簡化:
參考資料:Kotlin SAM 轉換

fun main() {
    runAction {
        println("使用 Lambda 執行")
    }
}

執行結果:

開始執行 runAction
使用 Lambda 執行
結束執行 runAction

建立一個 Object_3.kt 有以下寫法:

runAction {
    object : Action {
        override fun execute() {
            println("Executing implementation!")
        }
    }
}

這段 Lambda 的最後一行只是建立並回傳匿名物件,並沒有呼叫匿名物件的 execute();而 runAction() 需要的 Lambda 本身就是 Action.execute() 的內容,因此應直接寫成:
參考資料:Kotlin SAM 轉換

runAction {
    println("Executing implementation!")
}

二十、修正 DoAction 的 TODO 問題

原始 DoAction 內呼叫了 TODO("Not yet implemented")TODO() 的作用是直接丟出 NotImplementedError,所以程式執行到這一行就會中止。
參考資料:Kotlin TODO

原始程式:

class DoAction : Action {

    override fun execute() {
        println("Executing DoAction!")
        action()
    }

    private fun action() {
        TODO("Not yet implemented")
    }
}

完成後的修正版如下:
參考資料:Kotlin 函式式介面

class DoAction : Action {

    override fun execute() {
        println("Executing DoAction!")

        action()
    }

    private fun action() {
        // 【修正】完成實際功能,避免 TODO() 導致程式中止
        println("DoAction 的內部工作已完成")
    }
}

完整測試程式:

fun interface Action {
    fun execute()
}

fun runAction(action: Action) {
    println("開始執行 runAction")

    action.execute()

    println("結束執行 runAction")
}

class DoAction : Action {

    override fun execute() {
        println("Executing DoAction!")

        action()
    }

    private fun action() {
        // 【修正】不可保留 TODO(),否則執行時會發生例外
        println("DoAction 的內部工作已完成")
    }
}

fun main() {
    // 使用 Lambda 建立 Action
    runAction {
        println("Executing action 1")
    }

    println()

    // 使用具名類別建立 Action
    val myAction = DoAction()

    runAction(myAction)
}

二十一、is、as 與 as? 型別轉換

建立一父類別型別的變數若要存取子類別特有的 name 屬性,就需要先確認或轉換類型。
參考資料:Kotlin 型別檢查與轉型

val employee: Employee =
    FullTimeEmployee(
        name = "Mary",
        monthlySalary = 50_000.0
    )

如果確定物件一定是 FullTimeEmployee,可以使用 as
參考資料:Kotlin 強制轉型

val fullTimeEmployee =
    employee as FullTimeEmployee

println(fullTimeEmployee.name)

如果實際類型不符,as 會產生 ClassCastException。因此不確定類型時,應使用安全轉型 as?
參考資料:Kotlin 安全轉型

val partTimeEmployee =
    employee as? PartTimeEmployee

if (partTimeEmployee == null) {
    println("employee 不是 PartTimeEmployee")
}

更推薦使用 is 搭配智慧型轉型:
參考資料:Kotlin 智慧型轉型

when (employee) {
    is FullTimeEmployee -> {
        // 經過 is 判斷後,可直接存取 name
        println(
            "${employee.name} 的年薪是 " +
                employee.calculateSalary()
        )
    }

    is PartTimeEmployee -> {
        println(
            "${employee.name} 的薪資是 " +
                employee.calculateSalary()
        )
    }
}

二十二、完整綜合範例:不同付款方式

下面整合抽象類別、介面、多型、伴生物件、匿名物件與函式式介面,建立一個可直接執行的付款系統。
參考資料:Kotlin 介面Kotlin Object

// ==================================================
// 綜合範例
// 抽象類別、介面、單例、伴生物件、匿名物件、SAM
// ==================================================

// 付款結果的抽象父類別
abstract class PaymentResult {

    // 每種結果都必須提供訊息
    abstract val message: String

    // 一般方法可由所有子類別共用
    fun printResult() {
        println(message)
    }
}

// 付款成功
class PaymentSuccess(
    private val amount: Int
) : PaymentResult() {

    override val message: String
        get() = "付款成功,金額:$amount 元"
}

// 付款失敗
class PaymentFailure(
    private val reason: String
) : PaymentResult() {

    override val message: String
        get() = "付款失敗,原因:$reason"
}

// 所有付款方式必須遵守的介面
interface PaymentMethod {

    val methodName: String

    fun pay(amount: Int): PaymentResult
}

// 信用卡付款
class CreditCard(
    private val cardNumber: String
) : PaymentMethod {

    override val methodName: String = "信用卡"

    override fun pay(amount: Int): PaymentResult {
        if (amount <= 0) {
            return PaymentFailure(
                reason = "付款金額必須大於 0"
            )
        }

        if (cardNumber.length < 4) {
            return PaymentFailure(
                reason = "信用卡號格式錯誤"
            )
        }

        val lastFour = cardNumber.takeLast(4)

        println(
            "使用尾號 $lastFour 的信用卡付款"
        )

        return PaymentSuccess(amount)
    }
}

// 現金付款
class Cash : PaymentMethod {

    override val methodName: String = "現金"

    override fun pay(amount: Int): PaymentResult {
        if (amount <= 0) {
            return PaymentFailure(
                reason = "付款金額必須大於 0"
            )
        }

        println("收取現金:$amount 元")

        return PaymentSuccess(amount)
    }
}

// 付款紀錄器:整個程式共用同一個物件
object PaymentLogger {

    private var paymentCount: Int = 0

    fun log(
        methodName: String,
        result: PaymentResult
    ) {
        paymentCount++

        println(
            "第 $paymentCount 筆付款:$methodName"
        )

        result.printResult()
    }

    fun getPaymentCount(): Int {
        return paymentCount
    }
}

// 付款完成後的回呼介面
fun interface PaymentCallback {
    fun onComplete(result: PaymentResult)
}

// 結帳系統
class Checkout private constructor() {

    companion object {

        // 使用工廠方法建立 Checkout
        fun create(): Checkout {
            return Checkout()
        }
    }

    fun processPayment(
        amount: Int,
        method: PaymentMethod,
        callback: PaymentCallback
    ) {
        println("付款方式:${method.methodName}")

        val result = method.pay(amount)

        PaymentLogger.log(
            methodName = method.methodName,
            result = result
        )

        callback.onComplete(result)
    }
}

fun main() {
    val checkout = Checkout.create()

    val creditCard: PaymentMethod =
        CreditCard(
            cardNumber = "1234567812345678"
        )

    val cash: PaymentMethod = Cash()

    println("=== 第一筆付款 ===")

    checkout.processPayment(
        amount = 1_500,
        method = creditCard
    ) { result ->
        // 使用 is 判斷實際的付款結果
        when (result) {
            is PaymentSuccess -> {
                println("回呼通知:可以準備出貨")
            }

            is PaymentFailure -> {
                println("回呼通知:請重新付款")
            }
        }
    }

    println()
    println("=== 第二筆付款 ===")

    checkout.processPayment(
        amount = 500,
        method = cash
    ) { result ->
        result.printResult()
    }

    println()
    println("=== 第三筆付款:匿名付款方式 ===")

    // 建立只使用一次的匿名付款方式
    val couponPayment =
        object : PaymentMethod {

            override val methodName: String =
                "購物金"

            override fun pay(
                amount: Int
            ): PaymentResult {
                return if (amount in 1..1_000) {
                    PaymentSuccess(amount)
                } else {
                    PaymentFailure(
                        reason = "購物金最高只能折抵 1,000 元"
                    )
                }
            }
        }

    checkout.processPayment(
        amount = 800,
        method = couponPayment
    ) { result ->
        result.printResult()
    }

    println()
    println(
        "付款總筆數:" +
            PaymentLogger.getPaymentCount()
    )
}

二十三、Android 開發中的實際應用

今天的語法並不是單純的 Kotlin 練習,在 Android 專案中都有對應用途。
參考資料:Android Developers

Kotlin 功能 Android 常見用途
抽象類別 共用的 BaseActivityBaseFragment
介面 點擊事件、資料回呼、Repository 規格
多介面實作 同時支援點擊、拖曳、生命週期事件
巢狀類別 將只屬於某個類別的型別放在類別內
內部類別 存取 Activity 或 Adapter 外部成員
object Logger、資料管理器、共用設定
companion object TAG、常數、newInstance()
匿名物件 OnClickListener、Callback
fun interface 使用 Lambda 簡化單一方法介面
isas? 判斷 View、Fragment 或資料型別

Android 傳統 View 系統中的點擊監聽器就是常見例子:
參考資料:Android View.OnClickListener

button.setOnClickListener(
    object : View.OnClickListener {
        override fun onClick(view: View?) {
            println("按鈕被點擊")
        }
    }
)

因為 OnClickListener 只有一個抽象方法,所以可簡化成 Lambda:
參考資料:Android 點擊事件

button.setOnClickListener {
    println("按鈕被點擊")
}

二十四、今天的重點整理

今天學習的內容如下:
參考資料:Kotlin 官方文件

  1. abstract class 用來建立不能直接實例化的抽象父類別。
  2. 抽象屬性與抽象方法必須由普通子類別完成。
  3. 抽象類別可以包含建構子、狀態與一般方法。
  4. interface 用來定義類別必須遵守的規格。
  5. 介面可以宣告屬性、抽象方法與具有預設內容的方法。
  6. 一個類別只能直接繼承一個父類別,但能實作多個介面。
  7. 巢狀類別不會持有外部類別物件的參考。
  8. inner class 可以直接存取外部類別的成員。
  9. object 可建立整個程式共用的單例物件。
  10. companion object 可提供類別層級的常數與工廠方法。
  11. object : Type 可以建立一次性使用的匿名物件。
  12. fun interface 可以透過 SAM 轉換使用 Lambda。
  13. is 可搭配智慧型轉型安全判斷物件類型。
  14. as 轉型失敗會發生例外,不確定時應使用 as?
  15. Android 的事件監聽器、Fragment 與資料架構大量運用這些語法。

參考資料:Kotlin 介面Kotlin Object


二十五、今日作業

請設計一套通知系統,包含以下內容:
參考資料:Kotlin 介面

interface Notification {
    fun send(message: String): Boolean
}

請建立三個實作類別:

  • EmailNotification
  • SmsNotification
  • AppNotification

每一個類別都必須覆寫 send(),輸出不同的傳送方式。
參考資料:Kotlin 實作介面

再建立一個 NotificationManager 單例物件:

object NotificationManager {

    fun notify(
        message: String,
        notification: Notification
    ) {
        val result =
            notification.send(message)

        println(
            if (result) {
                "通知傳送成功"
            } else {
                "通知傳送失敗"
            }
        )
    }
}

最後分別傳入 Email、簡訊與 App 通知物件,觀察同一個 notify() 如何透過介面多型執行不同功能。
參考資料:Kotlin Object 宣告


結語

抽象類別負責建立共同基礎,介面負責規定物件具備哪些能力,object 負責提供共用的單一實例,而匿名物件與函式式介面則能簡化只使用一次的事件與回呼。掌握這些觀念後,就能寫出更容易擴充、測試與維護的 Kotlin 程式。
參考資料:Kotlin 官方文件

下一篇可以接續介紹 Kotlin 的資料類別 data class、列舉類別 enum class 與密封類別 sealed class,學習如何建立更安全、更容易管理的 App 資料模型。
參考資料:Kotlin Data ClassesKotlin Enum ClassesKotlin Sealed Classes


上一篇
從 Class 到多型——一次掌握 Kotlin 物件導向程式設計
下一篇
Kotlin Coroutine 入門:協程、CoroutineScope、SupervisorJob 與 async 非同步程式設計
系列文
Android 鐵人賽: 天命最高 - 陪伴大家一步步打造屬於自己的app13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言